归档说明:本文档概括当前项目的整体实现细节,涵盖游戏架构分层、流程控制原理、客户端单机实现、服务端权威实现、联机 FlowStep 同步协议及客户端消费层。内容自洽,可直接作为开发存档阅读,于2026.6.12日留存于博客备份。

目录

  1. 项目总览
  2. 整体架构分层
  3. GameCore 共享逻辑层
  4. 流程控制模块详解
  5. 客户端单机实现
  6. 服务端权威实现
  7. 联机 FlowStep 协议与同步模型
  8. 客户端联机消费层
  9. 双端对照与架构模式总结
  10. 扩展接入与调试验证
  11. 源码文件索引

1. 项目总览

《恶魔轮盘》是一款四人回合制卡牌博弈游戏,支持 Unity 客户端单机(人类 + 本地 AI)与 Unity WebGL / Standalone 联机(权威服务器 + 多人类 + AI 补位)两种运行模式。

技术栈概览:

层次 技术 职责
Unity 客户端 C# + UGUI/IMGUI 表现、输入、单机本地逻辑驱动
服务端 ASP.NET Core + SignalR Hub 登录、大厅、房间、权威对局
共享逻辑 GameCore(Assets/Scripts 链接至服务端) 规则、流程、技能、道具、AI 决策
协议 Protobuf(server/proto/v1/ 客户端与服务端消息 DTO
持久化 MySQL 账号、排行榜;房间与对局在内存

核心设计目标:

  1. 规则只写一份FlowController 在 Unity 单机与服务端联机共用,避免双端逻辑漂移。
  2. 流程与表现分离:流程层只发 PresentationCueSpec 和快照,不直接操作 UI。
  3. 联机防作弊:随机数、伤害、道具数量均在服务端 GameSession 内结算;客户端只收 FlowStep 展示。
  4. 可中断协程流程:用 IEnumerator + yield 表达「等玩家决策」,单机用 Unity 协程,服务端用 CoroutineDriver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
flowchart TB
subgraph Client["Unity 客户端"]
GE[GameEntry]
DS[DebugService]
HUD[NormalGameUIInputView / NormalHud]
NET[OnlineSession + OnlineHudCoordinator]
GE -->|单机| DS
GE -->|联机| NET
DS --> HUD
NET --> HUD
end

subgraph Server["ASP.NET 服务端"]
HUB[GameHub SignalR]
GLS[GameLoopService]
SDH[ServerDebugHost]
FP[FlowPublisher]
HUB --> GLS
GLS --> SDH
SDH --> FP
FP --> HUB
end

subgraph Core["GameCore 共享"]
GS[GameSession]
FC[FlowController]
SK[SkillExecutor]
IT[ItemService]
FC --> GS
GS --> SK
GS --> IT
end

DS --> FC
SDH --> FC
HUB <-->|FlowStep / SubmitDecision| NET

2. 整体架构分层

2.1 模块职责边界

模块 职责 禁止事项
FlowController 回合流转、五阶段状态机、响应链轮询、状态栈压栈/出栈 不直接画 UI、不读 WebSocket
GameSession 聚合玩家列表、枪膛、事件总线、技能执行器、回合指针 不决定 UI 如何展示
IDebugHost 决策输入、表现 cue 队列、快照推送(双端差异插槽) 不修改游戏规则
SkillExecutor / ItemService 响应事件、执行主动/被动技能、道具 CRUD 不推进回合阶段
AIDecisionMaker / GameDebugAiResolver 输出 PlayerDecision 不直接改实体状态
FlowPublisher(仅服务端) 合并 cue/战报/快照为 FlowStep 广播 不含游戏规则
OnlineHudCoordinator(仅联机客户端) revision 排序、表现门禁、权威态/显示投影 不跑 FlowController

2.2 服务端进程结构

1
2
3
4
5
6
7
NewRingGame.Server
├── Auth/ 登录、注册、Token 签发与校验
├── Lobby/ 在线玩家、房间列表
├── Room/ 房间生命周期、准备状态、AI 补位、自动开局
├── Game/ GameLoopService、ServerDebugHost、FlowPublisher、MatchStore
├── Connection/ WebSocket 连接映射、断线清理
└── Hubs/GameHub SignalR 实时消息入口

内存热数据(进程内字典,重启丢失):Session Token、ConnectionId→PlayerId、房间实例、进行中对局 MatchInstance(含 GameSession + seed)。MySQL 仅存账号与排行榜等冷数据。

2.3 客户端运行模式

GameEntry 通过三个布尔标志区分状态:

模式 IsMatchRunning IsOnlineMatch 流程引擎位置
单机正常/Debug true false 本地 Session.Flow.RunGameCoroutine()
联机对局 true true 不跑本地 FlowController,只消费 FlowStep
大厅/结算 false false

联机入口 BeginOnlineMatch 设置 IsOnlineMatch = true,由 NormalGameUIInputView.EnterOnlineMatch 初始化 HUD 与 OnlineHudCoordinator,等待服务端推送。

2.4 通信方式

  • HTTPS REST:登录(账号不存在则自动注册)、健康检查。
  • SignalR WebSocket(GameHub):大厅、房间、准备、对局 FlowStep 下行、SubmitDecision 上行。
  • Unity WebGL 必须使用 WebSocket(wss),因此服务端选用 SignalR 而非裸 TCP。

连接流程:客户端 POST /api/auth/login 获 Token → ConnectHub?access_token=xxxEnterLobby → 创建/加入房间 → 全员 Ready 后服务端 StartMatch → 广播 GameStarted + 首帧 FlowStep

3. GameCore 共享逻辑层

GameCore 即 Assets/Scripts 下无 UnityEngine 依赖(或 #if HEADLESS 隔离)的纯 C# 逻辑,服务端项目通过 csproj 链接同目录源码编译。

3.1 GameSession:对局会话聚合根

GameSession 持有单局全部可变状态与模块引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class GameSession
{
public Random Random { get; private set; }
public GameEventBus EventBus { get; } = new();
public GameStateStack StateStack { get; } = new();
public SkillExecutor SkillExecutor { get; } = new();
public ItemService ItemService { get; private set; }
public ChamberState Chamber { get; } = new();
public TurnPhase CurrentPhase { get; private set; }
public string TurnOwnerId { get; private set; }
public FlowController Flow { get; private set; }
public IDebugHost Debug { get; }

public GameSession(IDebugHost debug)
{
Debug = debug;
ItemService = new ItemService(SkillExecutor, Random);
AI = new AIDecisionMaker(this);
Flow = new FlowController(this);
}
}

SetupGame(seats, seed) 初始化流程:

  1. 注入 Random(seed)(联机可复现)或本地随机。
  2. 清空事件总线、状态栈、技能注册。
  3. SeatConfig 创建 PlayerEntity(座次、职业、ControlType、开局生命)。
  4. 注册全局被动(狂暴增伤、护身符格挡等)。
  5. 枪膛 Chamber.LoadRandom 装入实弹/空弹序列。
  6. 全员发放开局道具,批次写入 _startingDealBatches 供表现层发牌动画。

回合指针 _currentTurnIndex 按座次 0→1→2→3→0 顺时针流转;AdvanceTurn 跳过已淘汰玩家;仅剩一人存活时 SetGameOver

3.2 核心枚举

TurnPhase(五阶段)

1
TurnStart(0) → MainAction(1) → Judgment(2) → Discard(3) → TurnEnd(4)

FlowStateType(状态栈条目类型)

含义
TurnPhase 常规阶段(栈调试描述用)
ShootResponse 开枪响应窗口
DyingResponse 濒死自救
TraitorChoice 血量 1 点转职抉择

MainActionType:Shoot / UseItem / EndAction。

ControlType:Human / AI(断线时人类可切 AI 托管)。

3.3 GameStateStack:分层状态栈

1
2
3
4
5
6
7
8
public class GameStateStack
{
private readonly Stack<FlowStackEntry> _stack = new();
public FlowStackEntry Peek() => _stack.Count > 0 ? _stack.Peek() : null;
public void Push(FlowStackEntry entry) => _stack.Push(entry);
public FlowStackEntry Pop() => _stack.Count > 0 ? _stack.Pop() : null;
public int Count => _stack.Count;
}

规则:栈顶为当前唯一活跃流程;下层挂起保留协程断点;临时流程结算后 Pop 恢复。支持开枪响应内再嵌套濒死等多层场景。

3.4 事件总线与技能

GameEventBus 发布 GameEventType(TurnStart、ShootDeclared、DamageApplied、PlayerDying、ItemAfterUse 等)。角色技能与道具技能统一注册为 SkillExecutor 实例,被动技能订阅事件、主动技能由流程层在决策后调用 ExecuteActive

背叛者转职:玩家血量 1 且未抉择时进入 TraitorChoice 压栈;接受则 ConvertToTraitor 注销全部角色技能,仅保留开枪/道具基础权限。

3.5 PresentationCueRelay:权威表现指令

流程结算点不直接操作 HUD,而是构造 PresentationCueSpec 交给 IDebugHost.QueuePresentationCue

Cue 类型 触发场景
Shoot 开枪结算(含 bullet_type = FinalBullet)
ItemFly / StealHand / EjectDrink 道具使用
HealthDelta / HitImpact 血量变化
DealCard 发牌(回合开始或被动奖励)
MagicianTrajectory 魔术师弹道翻转
HideItemSlot 道具消耗前隐藏栏位(供 ItemFly 捕获起点)

CanEmit 条件:session.IsNormalPlayMode && session.Debug != null。Debug 面板模式跳过表现 cue,加速测试。

4. 流程控制模块详解

4.1 模块定位

FlowController 是游戏顶层调度中枢:负责回合流转、五阶段推进、全局状态栈、事件广播、响应链轮询。对标三国杀式「主流程 + 插入响应窗口」机制。

4.2 对局主循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public IEnumerator RunGameCoroutine()
{
yield return PresentStartingDealsIfAny();

while (!_session.IsGameOver)
{
var player = _session.GetCurrentTurnPlayer();
if (player == null) { _session.SetGameOver(); break; }

_session.SetTurnOwner(player);
player.ShootLockedThisTurn = false;

yield return RunTurnCoroutine(player);
if (_session.IsGameOver) break;

_session.AdvanceTurn();
PushAuthoritativeHudSnapshot(FlowBoundaryKind.TurnChange);
}
}

开局 PresentStartingDealsIfAny:取出 _startingDealBatches,经 PresentationCueRelay.EmitDealCards 播发牌动画,WaitActionPresentation 等待结束后再刷新 HUD。

4.3 单回合五阶段

1
2
3
4
5
6
7
8
public IEnumerator RunTurnCoroutine(PlayerEntity player)
{
yield return RunTurnStartPhase(player);
yield return RunMainActionPhase(player);
yield return RunJudgmentPhase(player);
yield return RunDiscardPhase(player);
RunTurnEndPhase(player);
}

各阶段要点:

阶段 行为 玩家响应
TurnStart 强化剂衰减、发回合道具(默认 2 件)、枪膛空则装弹、TurnStart 被动
MainAction 循环:开枪 / 用道具 / 结束行动 是(主行动决策)
Judgment 濒死结算、血量 1 转职扫描 是(濒死/转职)
Discard 道具超上限则循环弃牌 是(弃牌决策)
TurnEnd TurnEnd 被动、清理临时标记
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
flowchart TD
A[TurnStart 发道具装弹] --> B[MainAction 主行动循环]
B --> C{操作类型}
C -->|Shoot| D[压栈 ShootResponse]
D --> E[顺时针响应轮询]
E --> F[Pop 栈 + ResolveShoot]
F --> G{目标濒死?}
G -->|是| H[HandlePlayerDying 压栈]
H --> B
G -->|否| B
C -->|UseItem| I[ExecuteUseItem 即时结算]
I --> B
C -->|EndAction| J[Judgment 裁决]
J --> K[Discard 弃牌]
K --> L[TurnEnd]
L --> M[AdvanceTurn]

4.4 主行动决策循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private IEnumerator RunMainActionPhase(PlayerEntity player)
{
_session.Debug.NotifyPhase(_session, TurnPhase.MainAction, player);
var ended = false;
var failedShootAttempts = 0;

while (!ended && player.IsAlive)
{
PlayerDecision decision = null;
yield return _session.Debug.RequestMainActionDecision(player, _session, d => decision = d);
if (decision?.MainAction == null) continue;

switch (decision.MainAction)
{
case MainActionType.Shoot:
yield return ExecuteShoot(player, decision.TargetPlayerId, ...);
yield return WaitActionPresentation();
break;
case MainActionType.UseItem:
yield return ExecuteUseItem(player, decision, ...);
yield return WaitActionPresentation();
break;
case MainActionType.EndAction:
ended = true;
break;
}
}
}

连续无法开枪达到上限(MaxFailedShootAttemptsBeforeAutoEnd)时自动结束主行动。

4.5 开枪完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private IEnumerator ExecuteShoot(PlayerEntity shooter, string targetId, Action<bool> onExecuted)
{
// 校验锁枪、目标、Peek 初始子弹
var ctx = new ShootContext { ShooterId, TargetId, InitialBullet, FinalBullet };

_session.StateStack.Push(new FlowStackEntry
{
StateType = FlowStateType.ShootResponse,
Description = "开枪响应",
Payload = ctx
});

_session.Publish(GameEventType.ShootDeclared);
yield return RunShootResponseWindow(shooter, ctx);
_session.StateStack.Pop();
yield return ResolveShoot(shooter, target, ctx);
}

响应轮询规则RunShootResponseWindow):

  • 起点:当前回合玩家的顺时针下家。
  • 依次询问存活玩家;仅 魔术师且道具数 ≥ MagicianResponseMinItems 时轮询(ShouldPollShootResponse)。
  • 发动「弹道」主动技能则翻转 ctx.FinalBullet,播 MagicianTrajectory cue。

结算规则ResolveShoot):

  • Chamber.PopNext 出弹;实弹则 DamageAboutToApply 事件链 → 可能护身符格挡 → EnhancementItemService.ApplyDamage
  • 锁枪规则:自射空弹不锁;对他人开枪或任意实弹均锁枪(ShootLockedThisTurn = true)。
  • 实弹击杀:背叛者累计 TraitorKills;目标 Health≤0 进入 HandlePlayerDying
  • 结算后 PresentationCueRelay.EmitShootResolve(bullet 用 FinalBullet,非初始 Peek 值)。

4.6 濒死与转职

濒死HandlePlayerDying):压栈 DyingResponseRequestDyingDecision → 可选奶茶/道具自救 → 仍 ≤0 则清空道具并 Eliminate

转职HandleTraitorChoice):压栈 TraitorChoiceRequestTraitorDecision → 接受则 ConvertToTraitor

主行动中 ExecuteUseItem 后可调用 ProcessImmediateDying 即时处理濒死,不等到 Judgment 阶段。

4.7 弃牌阶段

RunDiscardPhase:当 player.ItemCount > player.GetItemCap() 时循环 RequestDiscardDecision,移除选中道具并记录战报。

4.8 PushAuthoritativeHudSnapshot 条件编译

1
2
3
4
5
6
private void PushAuthoritativeHudSnapshot(FlowBoundaryKind boundary = FlowBoundaryKind.ActionSettle)
{
#if HEADLESS
_session.Debug.PushAuthoritativeSnapshot(_session, boundary);
#endif
}

单机 Unity 编译时不调用(本地 HUD 由 DebugService.RefreshHud 驱动);服务端 HEADLESS 编译时每次行动/阶段/回合边界推送 FlowStep。

4.9 IDebugHost 接口契约

流程层通过以下方法与宿主交互:

方法 用途
RequestMainActionDecision 主行动:开枪/道具/结束
RequestShootResponseDecision 开枪响应(魔术师弹道)
RequestDyingDecision 濒死自救
RequestTraitorDecision 转职抉择
RequestDiscardDecision 超限弃牌
WaitActionPresentation 等待表现动画结束
QueuePresentationCue 入队表现 spec
PushAuthoritativeSnapshot 联机推送权威快照(服务端)
LogEvent 战报文本
NotifyPhase / RefreshHud 阶段与 HUD 元数据

5. 客户端单机实现

5.1 启动链路

1
2
3
4
5
6
7
8
// GameEntry.Start
Session = new GameSession(new DebugService());

// GameEntry.RunMatchCoroutine
Session.IsNormalPlayMode = LastPlayMode != PlayModeKind.Debug;
Session.HideOpponentHandItems = LastPlayMode == PlayModeKind.Master;
Session.SetupGame(seats, null);
yield return Session.Flow.RunGameCoroutine();

DebugService 实现 IDebugHost:承担 IMGUI Debug 面板或 NormalHud 的决策 UI、本地 cue 协程队列、战报日志。

5.2 决策路径

DebugService.RequestDecision 分支:

  1. AI 或托管AiDelegation.ShouldAutoDecide)→ ExecuteAiDecisionAIDecisionMaker 立即返回。
  2. 人类WaitHumanDecision:设置 _waitingHuman = true,UI 展示选项;玩家调用 SubmitHumanDecision(decision) 后协程继续。

Debug 模式支持控制台指令(shoot player2 | use item1 | end);正常模式由 NormalGameUIInputView 渲染按钮与道具栏。

5.3 表现播放

DebugService.QueuePresentationCue

  1. PresentationCuePlayback.TryBuildFromSpec 将 spec 转为 Unity 协程。
  2. 入队 _presentationCueQueue
  3. WaitActionPresentation 顺序 Dequeue 并 yield return 每个 cue 协程。
  4. 播放完毕后 FlushPendingHealthDeltas 等收尾。

单机无 revision 概念;流程 yield 等待本地动画自然结束后再开下一决策窗。

1
2
3
4
5
6
7
8
9
flowchart LR
FC[FlowController] --> PCR[PresentationCueRelay]
FC -->|Request*Decision| DS[DebugService]
PCR -->|QueuePresentationCue| DS
DS --> PCP[PresentationCuePlayback]
PCP --> FX[NormalHudPresentationFx]
DS --> HUD[NormalGameUIInputView]
HUD -->|SubmitHumanDecision| DS
DS --> FC

5.4 玩法模式差异

PlayModeKind 表现 手牌可见性
Simple NormalHud,无 Debug 面板 他人手牌可见
Master NormalHud HideOpponentHandItems=true,他人道具栏显示卡背
Debug 完整 Debug IMGUI + 可选 AI 托管 全信息

6. 服务端权威实现

6.1 对局启动:GameLoopService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void StartMatch(RoomModel room, GameStarted started)
{
var debugHost = new ServerDebugHost();
var session = new GameSession(debugHost)
{
IsNormalPlayMode = true,
HideOpponentHandItems = room.PlayMode == PlayMode.Master
};
var seatConfigs = RoomMatchSeatBuilder.Build(room, _connections, _matchOptions);
session.SetupGame(seatConfigs, started.Seed);

var match = new MatchInstance { MatchId, RoomId, Session, DebugHost, Seed, ... };
var publisher = new FlowPublisher(match, buildSnapshot, BroadcastFlowStepAsync);
match.FlowPublisher = publisher;
debugHost.FlowPublisher = publisher;

_matches.Add(match);
_ = RunMatchAsync(match);
}

RunMatchAsync

1
2
3
4
5
6
await match.FlowPublisher.PublishMatchStartAsync();
await CoroutineDriver.RunAsync(
match.Session.Flow.RunGameCoroutine(),
onIdle: null,
match.Cancellation.Token);
await FinishMatchAsync(match); // 排行榜计分 + GameOver 广播

6.2 MatchInstance 与 MatchStore

MatchInstance 持有:GameSessionServerDebugHostFlowPublisherRoomModelSeedCancellationTokenSource

PresentationSync 锁 + PresentationPipeline Task 链:串行化 FlowStep 广播,避免并发 Commit 乱序。

MatchStore 维护 MatchId / PlayerId / RoomId 三向索引,供 Hub 路由决策与断线处理。

6.3 ServerDebugHost:决策与推送

AI 即时决策

1
2
3
4
5
6
7
8
if (player.Control == ControlType.AI || AiDelegation.ShouldAutoDecide(player))
{
var ai = GameDebugAiResolver.Resolve(session, player, kind, shoot);
if (session.IsNormalPlayMode)
yield return CoroutineDelay.Seconds(AiThinkPauseSeconds); // 0.8s
onComplete(ai);
yield break;
}

人类等待 WebSocket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BeginWait(player, session, kind, shoot);  // 设置 PendingDecision
_ = FlowPublisher?.PublishDecisionOpenAsync();

while (_waiting)
{
if (超时 DecisionTimeoutMs) // 30s
{
_resolvedDecision = GameDebugAiResolver.Resolve(...);
_ = FlowPublisher?.PublishDecisionCloseAsync();
break;
}
yield return null;
}
onComplete(_resolvedDecision);

TrySubmitDecision(Hub 调用链:GameLoopService.TrySubmitDecision):

  • 校验 PendingDecision.Scene 与 payload 一致。
  • 校验 player_id 为当前等待者。
  • 主行动用道具时服务端预校验 MainActionRules.CanUseItem
  • 写入 _resolvedDecision_waiting = falsePublishDecisionCloseAsync

表现等待

1
2
3
4
5
6
public IEnumerator WaitActionPresentation(GameSession session)
{
var holdMs = FlowPublisher.LastPresentationHoldMs;
if (holdMs <= 0) yield break;
yield return CoroutineDelay.Seconds(holdMs / 1000f);
}

PushAuthoritativeSnapshot 映射 boundary 到 Publish 方法:

FlowBoundaryKind Publish 方法
ActionSettle PublishActionSettleAsync(含 hold)
TurnChange PublishTurnChangeAsync
PhaseChange PublishPhaseChangeAsync
DecisionOpen PublishDecisionOpenAsync
DecisionClose PublishDecisionCloseAsync
MatchStart PublishMatchStartAsync

6.4 FlowPublisher:revision 合并广播

核心约束:同一逻辑步内,先 BufferCue / BufferEvent,再 Publish,保证 cue 与 snapshot 同一 revision

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private Task PublishAsync(ProtoFlowBoundary boundary, bool includeHold)
{
_revision++;
var holdMs = includeHold ? PresentationHoldCalculator.ComputeMs(_bufferedCueSpecs) : 0;
LastPresentationHoldMs = holdMs;

var step = new FlowStep
{
Revision = _revision,
Boundary = boundary,
PresentationHoldMs = holdMs
};
// 填充 cues(带 flow_revision、cue_index)
// 填充 events(战报 GameEventLine)
// 填充 snapshot(GameProtoMapper.ToSnapshot,per-viewer 大师模式掩码)

_bufferedCueSpecs.Clear();
_bufferedEvents.Clear();
// PresentationPipeline 串行 await _broadcast
return _match.PresentationPipeline;
}

BuildResyncStep(viewerPlayerId):断线重连用,不递增 revision,boundary = MATCH_RESYNC。

大师模式(Master):BroadcastFlowStepPerViewerAsync 对每个真实玩家单独克隆 Step,掩码他人手牌事件文本与 cue,重建 per-viewer snapshot。

6.5 CoroutineDriver:无 Unity 协程运行时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static async Task RunAsync(IEnumerator root, Func<Task>? onIdle, CancellationToken ct)
{
var stack = new Stack<IEnumerator>();
stack.Push(root);
while (stack.Count > 0)
{
if (current.Current is IEnumerator nested) { stack.Push(nested); continue; }
if (current.Current is CoroutineDelay delay)
{
await Task.Delay(delay.Milliseconds, ct);
continue;
}
await Task.Delay(100, ct); // yield return null → 100ms,供 WS 决策与超时检测
}
}

6.6 断线、重连、超时

事件 服务端行为
人类断线 HandleDisconnectentity.Control = AI,广播最新 snapshot
重连 ResyncPlayerAsync:恢复 Human 控制,单播 MatchResynced(含完整 FlowStep + RoomUpdated)
决策 30s 超时 AI 代打,LogEvent [AI] 决策超时,PublishDecisionClose
对局异常 不广播 GameOver(避免多人存活误弹结算),Remove match,房间回 Waiting

6.7 PresentationHoldCalculator(双端共用)

Cue 类型 估算时长
Shoot 2800ms
ItemFly 1200ms
StealHand 1600ms
EjectDrink 1400ms
HealthDelta / HitImpact 1150ms
MagicianTrajectory 2000ms
DealCard 800ms × 张数
HideItemSlot 50ms
合计上下限 400ms ~ 6000ms

7. 联机 FlowStep 协议与同步模型

7.1 设计优先级

优先级 层级 职责
1 流程流转 回合/阶段/决策权何时转移;服务端 gate 下一决策
2 状态同步 血量、道具、枪膛、PendingDecision 等权威 HUD 数据
3 特效推送 开枪、道具飞行、回血/掉血飘字等纯表现

核心原则:同一逻辑步使用同一个 revision,cue 与 snapshot 捆绑下发,客户端按序处理。

7.2 FlowStep 消息结构

1
2
3
4
5
6
7
8
message FlowStep {
int64 revision = 1;
FlowBoundaryKind boundary = 2;
GameStateSnapshot snapshot = 3;
repeated PresentationCue cues = 4;
repeated GameEventLine events = 5;
int32 presentation_hold_ms = 6;
}

GameStateSnapshot 主要字段:

字段 含义
match_id / turn_owner_id / phase 对局与回合上下文
flow_state 状态栈顶描述(调试/Intel)
chamber 枪膛剩余、已知下一发、已消耗序列
players[] 各玩家血量、道具栏、ControlType、shoot_locked 等
pending_decision 当前等待决策的玩家、scene、timeout、wait_started_at
revision / boundary 与 FlowStep 对齐

PlayerDecisionPayload 上行字段:scene、main_action、target_player_id、item_instance_id、use_shoot_response_skill、use_dying_save、accept_traitor_conversion、items_to_discard 等。

Hub 消息种类:HUB_MESSAGE_KIND_FLOW_STEP = 111;重连 MatchResynced 含完整 FlowStep state(boundary = MATCH_RESYNC)。

旧版分通道 game_state_snapshot / presentation_cue / game_event 仍保留解析,FlowStep 启用后对局内应忽略(OnlineSession 丢弃 stale legacy 消息)。

7.3 FlowBoundaryKind 与客户端行为

boundary 含义 客户端行为
ACTION_SETTLE 一次行动结算完成 播 cues → 应用 snapshot;服务端按 hold 等待
TURN_CHANGE 回合切换 Abort 积压特效 → 全量刷新 HUD
PHASE_CHANGE 阶段切换 同上
DECISION_OPEN 打开决策窗 应用 snapshot + 展示决策 UI;不 abort 特效
DECISION_CLOSE 提交/关闭决策 关闭决策 UI;等后续 ACTION_SETTLE
MATCH_START 对局开始 Abort + 初始化 HUD
MATCH_RESYNC 断线重连 Abort + 直接应用 resync snapshot

7.4 服务端推送流水线

1
2
3
4
5
FlowController 结算点
├─ PresentationCueRelay.Emit* → ServerDebugHost.QueuePresentationCue → FlowPublisher.BufferCue
├─ LogEvent → FlowPublisher.BufferEvent
└─ PushAuthoritativeSnapshot → FlowPublisher.Publish*Async
└─ 合并 → FlowStep → SignalR 广播

决策推送:人类进入等待发 DECISION_OPEN(含 pending_decision);提交或超时发 DECISION_CLOSE。不再每 300ms 轮询推全量快照。

7.5 联机对局配置(OnlineMatchOptions)

配置 默认 说明
AiFillMax 3 AllowAiFill 开局最多补位 AI 数
AiStartingHealth 2 AI 开局生命(人类仍为 3)

固定四角座位(seatIndex 0–3),UI 旋转使本机永远在左上;空座隐藏即可。

8. 客户端联机消费层

联机客户端不运行 FlowController,职责是:按 revision 顺序接收 FlowStep → 更新权威态 → 播放 cue → 提交显示投影 → 在 DECISION_OPEN 时展示决策 UI → 上行 SubmitDecision。

8.1 组件链路

1
2
3
4
5
6
7
8
9
10
11
12
13
OnlineSession(Hub 消息入口)
└─ OnlineHudCoordinator(统一协调)
├─ OnlineFlowOrchestrator(revision 排序 + Presenting 门禁)
├─ OnlineHudState(权威态 vs 显示投影)
└─ NormalGameUIInputView.DispatchOnlineFlowStep
├─ 硬边界? → AbortQueuedPresentation + 清 gate
├─ BeginFlowStep + EnqueueCue(HideItemSlot 前捕获飞行起点)
├─ ApplyOnlineSnapshotData(写权威态)
├─ DecisionOpen/Close → 立即刷新决策 UI
├─ events → OnlineBattleLogStore 战报
└─ 表现 idle → CommitDisplay → NotifyPresentationIdle
└─ OnlineMatchPresentationPlayer(cue 队列 + hold 计时)
└─ PresentationCuePlayback → NormalHudPresentationFx

8.2 OnlineFlowOrchestrator

  • Enqueue(step, dispatch):revision ≤ lastApplied 丢弃;乱序入 SortedList 缓冲。
  • dispatch 返回 true → 设置 _presentationGated,暂停后续派发。
  • NotifyPresentationIdle → 清 gate,继续 TryDispatchSequential
  • ApplyResync / FastForwardToRevision:重连或追帧用。

8.3 OnlineHudState:双态模型

状态 变量 用途
权威态 _authoritative + _authoritativePlayers 决策合法性、Intel、PendingDecision
显示投影 _display 实际渲染 HUD 的快照
待提交 _pendingFullCommit 表现 busy 时排队,idle 后 CommitDisplay

表现播放中:权威态立即更新,显示投影延后,避免动画未播完道具栏已刷新导致穿帮。

OnlineHudCoordinator.ShouldDeferHudCommit 统一判定:决策边界不延后;硬边界不延后;表现 busy / orchestrator gated / 有待提交时延后。

8.4 客户端消费状态机

状态 行为
Idle Orchestrator 可派发下一 revision
Presenting 播放 cue 和/或 hold 计时;门禁后续 Step;权威态更新,显示排队
Applying 表现 idle → CommitDisplay → NotifyPresentationIdle

硬边界(TurnChange / PhaseChange / MatchStart / MatchResync)调用 AbortQueuedPresentation,清 gate,立即全量刷新。

8.5 DispatchOnlineFlowStep 处理顺序

  1. 判定 ShouldAbortPresentation → abort + 清 pending commit。
  2. BeginFlowStep:HideItemSlot cue 前 TryCaptureItemFlyStart
  3. Cue 入队 OnlineMatchPresentationPlayer
  4. SetAuthoritative(snapshot) 写权威态。
  5. DecisionOpen/Close 边界立即刷新决策 UI(不能等表现 idle,否则「提交中」状态卡住)。
  6. events 追加战报。
  7. 按规则 QueueFullCommit 或立即 CommitDisplay。

8.6 道具栏与 ItemFly 策略

  • 快照 players[].items 为权威数量。
  • 处理顺序:先 cue 入队,再写权威态;表现 busy 时不重建道具栏 DOM。
  • _onlinePresentationHiddenItems 仅本机 optimistic hide;观察他人以 snapshot 为准。
  • 发牌 pendingReveal 有 12s fallback(TryFinalizeOpeningDealPresentation)。

8.7 开枪特效规则

  • 权威子弹类型:PresentationCue.bullet_type = ctx.FinalBullet(魔术师翻转后的最终子弹)。
  • 同一 FlowStep 内 Shoot cue 入队成功后,禁止用战报 regex 二次播放。
  • 实弹 + 护身符:Shoot 仍播实弹动画,另播护身符反馈 cue,不播 HealthDelta。

8.8 重连 MatchResynced

1
2
3
4
Hub 收到 MatchResynced
→ QueueOnlineResync(EnterOnlineMatch 前暂存)
→ PrepareOnlineResync:Abort 表现 + BattleLog.ReplaceFromFlowStep + ApplyResync
→ EnterOnlineMatch 时若有 pending:从 state.events 重建战报

服务端 ResyncPlayerAsync 同时恢复 ControlType.HumanPublishSnapshotAsync 广播给他人。

8.9 双通道兼容

通道 入口 FlowStep 启用后
FlowStep FlowStepReceived 主路径
legacy snapshot SnapshotReceived 表现 busy 时 partial;校验 IsStaleLegacySnapshot
legacy cue/event PresentationCueReceived OnlineSession 丢弃,防重复

OnlineSession.UsesFlowStepChannel:收到首帧 FlowStep 后切换,丢弃 stale legacy。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sequenceDiagram
participant FC as FlowController
participant SDH as ServerDebugHost
participant FP as FlowPublisher
participant Hub as SignalR
participant OS as OnlineSession
participant OHC as OnlineHudCoordinator
participant HUD as NormalGameUIInputView

FC->>SDH: RequestMainActionDecision
SDH->>FP: PublishDecisionOpen
FP->>Hub: FlowStep rev=N boundary=DECISION_OPEN
Hub->>OS: FlowStepReceived
OS->>OHC: EnqueueFlowStep
OHC->>HUD: 展示决策 UI
HUD->>Hub: SubmitDecision
Hub->>SDH: TrySubmitDecision
SDH->>FP: PublishDecisionClose
FC->>FC: ExecuteShoot
FC->>SDH: QueuePresentationCue + PushSnapshot
SDH->>FP: PublishActionSettle hold=2800ms
FP->>Hub: FlowStep rev=N+1 cues+snapshot
Hub->>OHC: EnqueueFlowStep
OHC->>HUD: 播放开枪 + 应用权威态
FC->>SDH: WaitActionPresentation 2.8s
FC->>SDH: RequestMainActionDecision

9. 双端对照与架构模式总结

9.1 实现对照表

维度 客户端单机 服务端联机 客户端联机
流程引擎 本地 FlowController 同一份 FlowController 不运行
宿主 DebugService ServerDebugHost OnlineHudCoordinator
决策 IMGUI/NormalHud WS SubmitDecision 同左,经 Hub 上行
AI AIDecisionMaker GameDebugAiResolver 无(服务端算)
表现 本地 cue 协程 cue 进 FlowStep OnlineMatchPresentationPlayer
快照 RefreshHud 本地 FlowPublisher → proto ApplyOnlineSnapshotData
随机数 本地 Random seed 注入 不使用本地 Random
协程 Unity StartCoroutine CoroutineDriver Unity 仅播表现

9.2 三种架构模式组合

  1. 分层状态机 + 状态栈:主回合五阶段 + 开枪/濒死/转职插入流程。
  2. 协程脚本化工作流yield return Request*Decision 表达异步人机交互;服务端 CoroutineDriver 移植。
  3. 权威服务器 + 捆绑同步:逻辑步 = FlowStep(revision, boundary, snapshot, cues, events, hold)。

9.3 联机客户端不跑 FlowController 的原因

  1. 防作弊:Random、伤害、道具必须以服务端为准。
  2. 单点真相:避免客户端先算再上报的冲突。
  3. 表现可慢不可错:动画可延长,revision 严格单调。

10. 扩展接入与调试验证

10.1 新增动作接入步骤

  1. 流程层(GameCore):在 FlowController 结算点调用 PresentationCueRelay.Emit*;新决策场景扩展 IDebugHost.Request*Decision
  2. 表现时长PresentationCueKind + PresentationHoldCalculator.EstimateCueMs
  3. 协议:proto PresentationCueKind + PresentationCueMapper.ToProto
  4. Unity 表现PresentationCuePlayback.TryBuildFromSpec 新分支。
  5. 不要单独广播 snapshot/cue/event;统一 PushAuthoritativeSnapshot 一次 Commit。

10.2 调试要点

  • 观察 FlowStep.revision 是否严格单调递增。
  • 特效丢失:检查 cue 是否为空、TryBuildFromSpec 是否 false。
  • 道具数量错乱:检查 snapshot.items 与是否误用 hidden 集合过滤他人。
  • 服务端:确认 BufferCue 在 Publish 之前完成。

10.3 自动化测试

客户端 FlowSync 单元测试(无需启动服务端):

1
2
cd server/tools/ProtoSmokeTest
dotnet run -- --flow-sync-unit

覆盖:OnlineFlowOrchestrator 排序/门禁、OnlineHudState 显示延后、OnlineFlowSync 规则。

联机对局时序冒烟(需 API + 自动对局):

1
2
cd server/tools/ProtoSmokeTest
dotnet run -- --match-flow --minutes 8 http://127.0.0.1:8080 用户名 密码

校验:revision 连续、bundle 内 snapshot/cue revision 对齐、ActionSettle 含 cue 时 hold>0、无 legacy 分通道消息。

Proto 代码生成:

1
cd server && ./scripts/generate-proto.sh

11. 源码文件索引

GameCore(Assets/Scripts,服务端链接编译)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Flow/FlowController.cs           流程主控:五阶段、响应链、压栈
Flow/GameSession.cs 对局会话聚合
Flow/PresentationCueRelay.cs 权威表现 cue 发射
Flow/PresentationHoldCalculator.cs 表现时长估算
Flow/FlowBoundaryKind.cs 边界枚举(与 proto 对齐)
Core/GameStateStack.cs 分层状态栈
Core/GameEnums.cs TurnPhase, FlowStateType, MainActionType
Core/GameEventBus.cs 全局事件总线
Debug/IDebugHost.cs 双端宿主接口
Debug/DebugService.cs Unity 单机宿主
Character/PlayerEntity.cs 玩家实体
Item/ItemService.cs 道具服务
Skill/SkillExecutor.cs 技能执行
AI/AIDecisionMaker.cs 单机 AI 决策
Game/GameEntry.cs 单机/联机入口

服务端(server/src/NewRingGame.Server)

1
2
3
4
5
6
7
8
9
Services/Game/GameLoopService.cs     对局启动、决策路由、断线重连
Services/Game/ServerDebugHost.cs IDebugHost 服务端实现
Services/Game/FlowPublisher.cs FlowStep 合并广播
Services/Game/CoroutineDriver.cs 无 Unity 协程驱动
Services/Game/GameProtoMapper.cs GameSession → proto snapshot
Services/Game/GameDecisionMapper.cs proto ↔ PlayerDecision
Services/Game/MatchModels.cs MatchInstance、MatchStore
Services/Game/MasterModePrivacy.cs 大师模式掩码
Hubs/GameHub.cs SignalR 入口

协议

1
2
3
server/proto/v1/game.proto           FlowStep、GameStateSnapshot、PlayerDecisionPayload
server/proto/v1/enums.proto FlowBoundaryKind、HubMessageKind
server/proto/v1/room.proto 房间与座位

联机客户端(Assets/Scripts/Network + UI)

1
2
3
4
5
6
7
8
9
10
11
12
Network/OnlineSession.cs             Hub 连接、消息分发
Network/OnlineHudCoordinator.cs 协调 Orchestrator + HudState
Network/OnlineFlowOrchestrator.cs revision 排序与门禁
Network/OnlineHudState.cs 权威态/显示投影双态
Network/OnlineFlowSync.cs 边界判定与延后规则
Network/OnlineFlowChannel.cs FlowStep/legacy 通道切换
Network/OnlineBattleLogStore.cs 战报存储
Network/GameHubClient.cs SignalR 客户端
UI/NormalGameUIInputView.Online.cs 联机 FlowStep 派发、决策 UI
UI/OnlineMatchPresentationPlayer.cs cue 队列与 hold
UI/PresentationCuePlayback.cs cue spec → 动画协程
UI/NormalHudPresentationFx.cs HUD 特效实现

文档版本:GameCore 共用 FlowController + FlowStep 联机方案。归档日期:2026-06。